use godot::{
    engine::{
        mesh::{ArrayType, PrimitiveType},
        ArrayMesh, IMeshInstance3D, MeshInstance3D,
    },
    obj::IndexEnum,
    prelude::*,
};

use crate::Dir;

#[derive(GodotClass)]
#[class(init, base = Resource)]
pub struct ChunkData {
    #[export]
    size: Vector3i,
    #[export]
    data: PackedByteArray,
}

#[godot_api]
impl ChunkData {
    #[func]
    pub fn contains(&self, x: i64, y: i64, z: i64) -> bool {
        (0..self.width() as i64).contains(&x)
            && (0..self.height() as i64).contains(&y)
            && (0..self.length() as i64).contains(&z)
    }

    #[func]
    pub fn resize(&mut self, x: i32, y: i32, z: i32) {
        self.size = Vector3i::new(x, y, z);
        self.data
            .resize(self.width() * self.height() * self.length());
    }

    #[func]
    pub fn clear(&mut self) {
        self.data.clear();
    }

    #[func]
    pub fn set_block(&mut self, x: i64, y: i64, z: i64, block: i64) {
        if !self.contains(x, y, z) {
            return;
        }
        
        let idx = x as usize
        + z as usize * self.size.x as usize
        + y as usize * (self.size.x * self.size.z) as usize;
        self.data[idx] = block as u8;
        
        godot_print!("set {idx}({x}, {y}, {z}) to {block}")
    }

    #[func]
    pub fn get_block(&self, x: i64, y: i64, z: i64) -> i64 {
        if !self.contains(x, y, z) {
            return -1
        }

        let idx = x as usize
            + z as usize * self.size.x as usize
            + y as usize * (self.size.x * self.size.z) as usize;
        return self.data[idx] as _
    }
}

impl ChunkData {
    pub fn is_transparent(&self, x: i32, y: i32, z: i32) -> bool {
        if !self.contains(x as _, y as _, z as _) {
            return true
        }

        let idx = x as usize + z as usize * self.size.x as usize + y as usize * (self.size.x * self.size.z) as usize;
        self.data[idx] == 0
    }
}

trait IChunkData {
    fn size(&self) -> Vector3i;
    fn is_transparent(&self, x: i32, y: i32, z: i32) -> bool;

    fn width(&self) -> usize {
        self.size().x as _
    }

    fn height(&self) -> usize {
        self.size().y as _
    }

    fn length(&self) -> usize {
        self.size().z as _
    }
}

impl IChunkData for ChunkData {
    fn size(&self) -> Vector3i {
        self.size
    }

    fn is_transparent(&self, x: i32, y: i32, z: i32) -> bool {
        self.is_transparent(x, y, z)
    }
}

impl IChunkData for Gd<ChunkData> {
    fn size(&self) -> Vector3i {
        self.bind().size
    }

    fn is_transparent(&self, x: i32, y: i32, z: i32) -> bool {
        self.bind().is_transparent(x, y, z)
    }
}

#[derive(GodotClass)]
#[class(tool, init, base = MeshInstance3D)]
pub struct ChunkIntance {
    base: Base<MeshInstance3D>,
    #[export]
    data: Option<Gd<ChunkData>>,
}

#[godot_api]
impl IMeshInstance3D for ChunkIntance {
    fn ready(&mut self) {
        if self.get_data().is_some() {
            self.generate_mesh(self.get_data());
        }
    }
}

#[godot_api]
impl ChunkIntance {
    #[func]
    pub fn generate_mesh(&mut self, data: Option<Gd<ChunkData>>) {
        if data.is_none() {
            godot_print!("data is None!");
            return
        }

        let data = data.unwrap();

        let mut mesh = self
            .base()
            .get_mesh()
            .map(Gd::cast)
            .unwrap_or(ArrayMesh::new_gd());

        mesh.clear_surfaces();

        let mut v = PackedVector3Array::new();
        let mut i = PackedInt32Array::new();
        let mut n = PackedVector3Array::new();

        for y in 0..data.height() as i32 {
            for z in 0..data.length() as i32 {
                for x in 0..data.width() as i32 {
                    if data.is_transparent(x, y, z) {
                        continue;
                    }

                    if data.is_transparent(x - 1, y, z) {
                        write_face(&mut v, &mut i, &mut n, x, y, z, Dir::Left);
                    }

                    if data.is_transparent(x + 1, y, z) {
                        write_face(&mut v, &mut i, &mut n, x, y, z, Dir::Right);
                    }

                    if data.is_transparent(x, y - 1, z) {
                        write_face(&mut v, &mut i, &mut n, x, y, z, Dir::Down);
                    }

                    if data.is_transparent(x, y + 1, z) {
                        write_face(&mut v, &mut i, &mut n, x, y, z, Dir::Up);
                    }

                    if data.is_transparent(x, y, z - 1) {
                        write_face(&mut v, &mut i, &mut n, x, y, z, Dir::Forward);
                    }

                    if data.is_transparent(x, y, z + 1) {
                        write_face(&mut v, &mut i, &mut n, x, y, z, Dir::Back);
                    }
                }
            }
        }

        if v.len() == 0 {
            return
        }

        let mut surface = Array::new();
        surface.resize(ArrayType::MAX.to_index(), &Variant::nil());
        surface.set(ArrayType::VERTEX.to_index(), v.to_variant());
        surface.set(ArrayType::INDEX.to_index(), i.to_variant());
        surface.set(ArrayType::NORMAL.to_index(), n.to_variant());

        mesh.add_surface_from_arrays(PrimitiveType::TRIANGLES, surface);

        self.base_mut().set_mesh(mesh.upcast());
    }
}

fn write_face(
    vertices: &mut PackedVector3Array,
    indices: &mut PackedInt32Array,
    normals: &mut PackedVector3Array,
    x: i32,
    y: i32,
    z: i32,
    dir: Dir,
) {
    let i = vertices.len() as _;
    let o = Vector3::new(x as _, y as _, z as _);

    match dir {
        Dir::Left => {
            vertices.push(o);
            vertices.push(o + Vector3::UP);
            vertices.push(o + Vector3::UP + Vector3::BACK);
            vertices.push(o + Vector3::BACK);

            normals.extend([Vector3::LEFT; 4]);
        }
        Dir::Right => {
            vertices.push(o + Vector3::RIGHT + Vector3::BACK);
            vertices.push(o + Vector3::ONE);
            vertices.push(o + Vector3::RIGHT + Vector3::UP);
            vertices.push(o + Vector3::RIGHT);

            normals.extend([Vector3::RIGHT; 4]);
        }
        Dir::Up => {
            vertices.push(o + Vector3::UP);
            vertices.push(o + Vector3::UP + Vector3::RIGHT);
            vertices.push(o + Vector3::ONE);
            vertices.push(o + Vector3::UP + Vector3::BACK);

            normals.extend([Vector3::UP; 4]);
        }
        Dir::Down => {
            vertices.push(o);
            vertices.push(o + Vector3::BACK);
            vertices.push(o + Vector3::BACK + Vector3::RIGHT);
            vertices.push(o + Vector3::RIGHT);

            normals.extend([Vector3::DOWN; 4]);
        }
        Dir::Forward => {
            vertices.push(o + Vector3::RIGHT);
            vertices.push(o + Vector3::RIGHT + Vector3::UP);
            vertices.push(o + Vector3::UP);
            vertices.push(o);

            normals.extend([Vector3::FORWARD; 4]);
        }
        Dir::Back => {
            vertices.push(o + Vector3::BACK);
            vertices.push(o + Vector3::BACK + Vector3::UP);
            vertices.push(o + Vector3::ONE);
            vertices.push(o + Vector3::BACK + Vector3::RIGHT);

            normals.extend([Vector3::BACK; 4]);
        }
    }
    indices.push(i);
    indices.push(i + 1);
    indices.push(i + 2);
    indices.push(i);
    indices.push(i + 2);
    indices.push(i + 3);
}
